// Copyright (c) 2022 Presto Labs Pte. Ltd.
// Author: hyuan

#include "coin2/exchange/coincheck/order_v1/parser.h"

#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "coin2/base/conversion.h"
#include "coin2/base/log.h"
#include "coin2/exchange/coincheck/order_v1/error_code.h"
#include "coin2/exchange/util/json_util.h"

namespace coin2::exchange::coincheck::order_v1 {

namespace {

using namespace coin2::exchange::coincheck::order_v1::error_code;

using coin::proto::AccountBalance;
using coin::proto::AccountPosition;
using coin::proto::CancelOrderResponse;
using coin::proto::CurrencyTransferElement;
using coin::proto::FillType;
using coin::proto::ProductFillBundle;
using coin::proto::ProductOrderBundle;
using coin::proto::ProductOrderElement;
using coin::proto::SubmitOrderResponse;
using coin2::exchange::base::order::IOrderContext;
using coin2::exchange::coincheck::symbology::CoincheckCurrency;
using coin2::exchange::coincheck::symbology::CoincheckProduct;

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;

inline coin::proto::OrderSide GetOrderSide(const std::string& type) {
  if (type == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (type == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

bool IsCoincheckSpotErrMessage(const JsonValue& doc) {
  if (!doc.HasMember("success")) {
    return true;
  }

  return doc["success"].GetBool() == false;
}

}  // namespace

void CoincheckParser::ParseRestAccountBalance() {
  /*
  {
    "success": true,
    "jpy": "0.8401",
    "btc": "7.75052654",
    "jpy_reserved": "3000.0",
    "btc_reserved": "3.5002",
    "jpy_lend_in_use": "0",
    "btc_lend_in_use": "0.3",
    "jpy_lent": "0",
    "btc_lent": "1.2",
    "jpy_debt": "0",
    "btc_debt": "0"
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestAccountBalance] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

// NOTE: when field exchange and market_type are set, which means parsed successfully
AccountBalance CoincheckParser::ParseBalance(const JsonValue& doc) {
  RawBalanceMap raw_balance_map;
  auto success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }

  AccountBalance acc_balance;
  acc_balance.set_exchange("Coincheck");
  acc_balance.set_market_type("Spot");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeLower()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available();
  }

  return acc_balance;
}

bool CoincheckParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  if (doc.IsObject() && IsCoincheckSpotErrMessage(doc)) {
    LOG(ERROR) << "[ParseBalance] Error message\n" << JsonToString(doc);
    return false;
  }

  std::vector<std::string> currencies;
  for (auto itr = doc.MemberBegin(); itr != doc.MemberEnd(); ++itr) {
    std::string_view member = itr->name.GetString();
    if ((member.size() == 3) || (member.size() == 4)) {
      currencies.emplace_back(member);
    }
  }

  for (const auto& ccy : currencies) {
    std::string ccy_reserved = ccy + "_reserved";
    std::string ccy_lend_in_use = ccy + "_lend_in_use";
    std::string ccy_lent = ccy + "_lent";
    std::string ccy_debt = ccy + "_debt";
    if (!doc.HasMember(ccy_reserved.c_str()) || !doc.HasMember(ccy_lend_in_use.c_str()) ||
        !doc.HasMember(ccy_lent.c_str()) || !doc.HasMember(ccy_debt.c_str())) {
      continue;
    }

    CurrencyBalance balance;
    balance.set_currency_native(ccy);
    auto avail = stod(doc[ccy.c_str()].GetString());
    auto hold = stod(doc[ccy_reserved.c_str()].GetString()) +
                stod(doc[ccy_lend_in_use.c_str()].GetString()) +
                stod(doc[ccy_lent.c_str()].GetString()) + stod(doc[ccy_debt.c_str()].GetString());
    balance.set_available(avail);
    balance.set_total(avail + hold);
    balance.set_hold(hold);
    raw_balance_map->emplace(balance.currency_native(), balance);
  }

  return true;
}

void CoincheckParser::ParseRestOpenOrderInfoList() {
  /*
  {
    "success": true,
    "orders": [
    {
      "id": 202835,
      "order_type": "buy",
      "rate": 26890,
      "pair": "btc_jpy",
      "pending_amount": "0.5527",
      "pending_market_buy_amount": null,
      "stop_loss_rate": null,
      "created_at": "2015-01-10T05:55:38.000Z"
    }]
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsCoincheckSpotErrMessage(document)) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] Error message\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("orders") || !document["orders"].IsArray()) {
    LOG(ERROR) << "[ParseRestOpenOrderInfoList] Invalid data format\n" << JsonToString(document);
    return;
  }

  auto product_order_bundle = result_.mutable_order_list();
  product_order_bundle->Clear();
  bool product_order_bundle_found = false;
  for (const auto& data : document["orders"].GetArray()) {
    auto order = product_order_bundle->add_each_order();
    order->set_exchange_type(ExchangeType::Coincheck);
    order->set_market_type(MarketType::Spot);
    order->set_api_version("v1");
    order->set_exchange_order_id(data["id"].GetString());
    order->set_symbol(data["pair"].GetString());
    order->set_price(stod(data["rate"].GetString()));
    order->set_qty(std::fabs(stod(data["pending_amount"].GetString())));
    order->set_side(GetOrderSide(data["order_type"].GetString()));
    order->set_timestamp(msg_->timestamp);
    order->set_is_live(true);
    product_order_bundle_found = true;
  }

  if (product_order_bundle_found) {
    result_.set_type(MsgType::ORDER_STATUS);
  }
}

void CoincheckParser::ParseRestDoneOrderInfoList() {
  /*
  {
    "success": true,
    "id": 4766350773,
    "cancel": false,
    "created_at": "2022-07-04T02:15:19.000Z"
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestDoneOrderInfoList] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsCoincheckSpotErrMessage(document)) {
    LOG(ERROR) << "[ParseRestDoneOrderInfoList] Error message\n" << JsonToString(document);
    return;
  }

  std::string order_id = document["id"].GetString();
  const auto* oc = ocm_->FindByExchangeOrderId(order_id);
  bool is_order_done = false;
  if (oc && oc->is_alive()) {
    bool is_canceled = document["cancel"].GetBool();
    if (is_canceled) {
      result_.set_type(MsgType::ORDER_STATUS);
      auto product_order_bundle = result_.mutable_order_list();
      product_order_bundle->Clear();
      auto order = product_order_bundle->add_each_order();
      order->set_exchange_type(ExchangeType::Coincheck);
      order->set_market_type(MarketType::Spot);
      order->set_api_version("v1");
      order->set_exchange_order_id(order_id);
      order->set_symbol(oc->native_product());
      order->set_price(oc->order_price());
      order->set_qty(oc->order_qty());
      order->set_side(oc->order_side());
      order->set_timestamp(msg_->timestamp);
      order->set_is_live(false);
      order->set_cancel_confirmed(true);
      is_order_done = true;
    } else {
      result_.set_type(MsgType::IGNORABLE);
    }
  } else {
    result_.set_type(MsgType::IGNORABLE);
    is_order_done = true;
  }
  if (is_order_done) {
    mb_->PublishEvent("coincheck/order_cancel_status_del", reinterpret_cast<void*>(&order_id));
  }
}

void CoincheckParser::ParseRestFillOrderInfoList() {
  /*
  {
    "success": true,
    "transactions": [
    {
      "id": 38,
      "order_id": 49,
      "created_at": "2015-11-18T07:02:21.000Z",
      "funds": {
        "btc": "0.1",
        "jpy": "-4096.135"
      },
      "pair": "btc_jpy",
      "rate": "40900.0",
      "fee_currency": "JPY",
      "fee": "6.135",
      "liquidity": "T",
      "side": "buy"
    }]
  }
  */
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestFillOrderInfoList] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (document.IsObject() && IsCoincheckSpotErrMessage(document)) {
    LOG(ERROR) << "[ParseRestFillOrderInfoList] Error message\n" << JsonToString(document);
    return;
  }

  if (!document.HasMember("transactions") || !document["transactions"].IsArray()) {
    LOG(ERROR) << "[ParseRestFillOrderInfoList] Invalid data format\n" << JsonToString(document);
    return;
  }

  auto product_fill_bundle = result_.mutable_fill_list();
  product_fill_bundle->Clear();
  bool product_fill_bundle_found = false;
  for (const auto& data : document["transactions"].GetArray()) {
    auto fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(data["pair"].GetString());
    fill->set_exchange_order_id(data["order_id"].GetString());
    fill->set_price(stod(data["rate"].GetString()));
    fill->set_qty(0);
    std::vector<std::string> splitted;
    splitted = Split(data["pair"].GetString(), "_");
    if (splitted.size() == 2) {
      if (data["funds"].HasMember(splitted[0].c_str())) {
        fill->set_qty(std::fabs(stod(data["funds"][splitted[0].c_str()].GetString())));
      } else {
        LOG(ERROR) << "[ParseRestFillOrderInfoList] Invalid funds': "
                   << JsonToString(data["funds"]);
      }
    } else {
      LOG(ERROR) << "[ParseRestFillOrderInfoList] Invalid pair: " << data["pair"].GetString();
    }
    fill->set_fill_id(data["id"].GetString());
    if (data.HasMember("fee") && !data["fee"].IsNull()) {
      fill->set_fee(stod(data["fee"].GetString()));
    }
    if (data.HasMember("fee_currency") && !data["fee_currency"].IsNull()) {
      fill->set_fee_currency(data["fee_currency"].GetString());
    }
    fill->set_fill_timestamp(TimestampFromOkexIso8601(data["created_at"].GetString()));
    product_fill_bundle_found = true;
  }

  if (product_fill_bundle_found) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void CoincheckParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(GetCurrentTimestamp());

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseSubmitResponse] AsioHttpContextStatus error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[ParseSubmitResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (!json.HasMember("success") || json["success"].IsFalse()) {
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR);
    resp->set_exchange_error_code("");
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    LOG(ERROR) << "[ParseSubmitResponse] Error response\n" << http_context->res;
    resp->set_success(false);
    return;
  }

  resp->set_success(true);
  if (json.HasMember("id")) {
    resp->set_exchange_order_id(std::string(json["id"].GetString()));
  } else {
    LOG(ERROR) << "[ParseSubmitResponse] Invalid data format\n" << JsonToString(json);
  }
}

void CoincheckParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(msg_->timestamp);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseCancelResponse] AsioHttpContextStatus error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[ParseCancelResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (!json.HasMember("success") || json["success"].IsFalse()) {
    auto error_code = coin::proto::OrderErrorCode::ORDER_UNKNOWN_ERROR;
    std::string exchange_error_msg = GetExchangeOrderErrorMsg(json);
    if (boost::ifind_first(exchange_error_msg, "The order doesn't exist")) {
      error_code = coin::proto::OrderErrorCode::ORDER_NOT_FOUND;
    } else if (boost::ifind_first(exchange_error_msg, "Failed to cancel the order")) {
      error_code = coin::proto::OrderErrorCode::ORDER_FILLED_ALREADY;
    } /* else if (boost::ifind_first(exchange_error_msg, "cancel order temporally disabled")) {
    } */
    resp->set_error_code(error_code);
    resp->set_exchange_error_code("");
    resp->set_exchange_error_msg(exchange_error_msg);
    LOG(ERROR) << "[ParseCancelResponse] Error response\n" << http_context->res;
    resp->set_success(false);
    return;
  }

  resp->set_success(true);
  resp->set_cancel_confirmed(false);

  // query cancel status
  std::string order_id = resp->exchange_order_id();
  mb_->PublishEvent("coincheck/order_cancel_status_add", reinterpret_cast<void*>(&order_id));
}

int32_t CoincheckParser::ParseErrorMessage(const std::shared_ptr<AsioHttpContext>& http_context) {
  enable_time_.success = false;

  const auto http_code = http_context->res.result_int();
  if (http_code == 429) {
    const auto& header = http_context->res.base();
    if (header.count("retry-after") > 0) {
      int32_t retry_after = std::stoi(header.at("retry-after").data());
      if (retry_after > 0) {
        enable_time_.enable_time = (retry_after + 2) * 1'000'000'000LL + GetCurrentTimestamp();
        enable_time_.success = true;
        LOG(INFO) << "coincheck native client enable_time: " << enable_time_.enable_time;
      }
    }
    LOG(ERROR) << "[ParseErrorMessage] Too Many Requests\n" << msg_->DebugString();
  }

  return http_code;
}

void CoincheckParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  NOTREACHED() << "[ParseWsMessage] WebSocket not supported";
}

auto CoincheckParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg_->type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  CHECK(msg.type == OrderSubTopicId::REST_MESSAGE);

  int32_t error_code = ParseErrorMessage(msg.http_context);
  if (error_code == 429) {
    if (enable_time_.success) {
      mb_->PublishEvent("coincheck/enable_time", reinterpret_cast<void*>(enable_time_.enable_time));
    }
  }

  switch (msg.http_context->query_type) {
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestAccountBalance();
      break;
    case OrderSubTopicId::REST_OPEN_ORDERS:
      ParseRestOpenOrderInfoList();
      break;
    case OrderSubTopicId::REST_DONE_ORDERS:
      ParseRestDoneOrderInfoList();
      break;
    case OrderSubTopicId::REST_FILL_ORDERS:
      ParseRestFillOrderInfoList();
      break;
    default: {
      LOG(ERROR) << "[ParseRawMessage] Unknown query type\n" << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::coincheck::order_v1
